-
Notifications
You must be signed in to change notification settings - Fork 442
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/multiple templates arguments #1096
base: main
Are you sure you want to change the base?
Feature/multiple templates arguments #1096
Conversation
Co-authored-by: Rob Sterner <[email protected]>
86b4392
to
666913c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for picking this up @jsolas! I've added a few comments.
|
||
The generated methods are only invokable via keyword arguments | ||
|
||
The generated methods cannot have arguments with default values. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, but they can be simulated by the view.
|
||
The generated methods cannot have arguments with default values. | ||
|
||
The generated methods are public, and thus could be invoked by a third party. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be changed, but not sure if it's worth it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be nice to introduce them as private to start.
fcdace0
to
37dd2fb
Compare
37dd2fb
to
dff7c0d
Compare
Hello @BlakeWilliams, @camertron, @joelhawksley, i tried to do what was discussed in #387 (comment), I would like to receive your opinions and see if there is a possibility of moving forward with this. Thanks. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good! I think we're on the right track here so I've marked the ADR as accepted.
@@ -0,0 +1,88 @@ | |||
# 1. Allow multiple templates |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need a name for this feature, I think. What about:
# 1. Allow multiple templates | |
# 3. Template parts |
|
||
## Status | ||
|
||
Proposed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Proposed. | |
Accepted |
As components become larger (for example, because you are implementing a whole page), it becomes | ||
useful to be able to extract sections of the view to a different file. ActionView has | ||
partials, and ViewComponent lacks a similar mechanism. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As components become larger (for example, because you are implementing a whole page), it becomes | |
useful to be able to extract sections of the view to a different file. ActionView has | |
partials, and ViewComponent lacks a similar mechanism. | |
As components become larger (for example, implementing a whole page), it can become useful to be able to extract sections of the view to a different file. ActionView has partials, and ViewComponent lacks a similar mechanism. |
ActionView partials have the problem that their interface is not introspectable. Data | ||
may be passed into the partial via ivars or locals, and it is impossible to know | ||
which without actually opening up the file. Additionally, partials are globally | ||
invocable, thus making it difficult to detect if a given partial is in use or not, | ||
and who are its users. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ActionView partials have the problem that their interface is not introspectable. Data | |
may be passed into the partial via ivars or locals, and it is impossible to know | |
which without actually opening up the file. Additionally, partials are globally | |
invocable, thus making it difficult to detect if a given partial is in use or not, | |
and who are its users. | |
ActionView partials have the problem that their interface is not introspectable. Data may be passed into the partial via ivars or locals, and it is impossible to know which without actually opening up the file. Additionally, partials are globally invocable, thus making it difficult to detect if a given partial is in use or not, and who are its users. |
Allow multiple ERB templates available within the component, and make it possible to | ||
invoke them from the main view.Templates are compiled to methods in the format `render_#{template_basename}(locals = {})` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allow multiple ERB templates available within the component, and make it possible to | |
invoke them from the main view.Templates are compiled to methods in the format `render_#{template_basename}(locals = {})` | |
Allow multiple ERB templates available within the component, and make it possible to invoke them from the main view. Templates are compiled to methods in the format `render_#{template_basename}(locals = {})`. |
@@ -36,6 +36,10 @@ title: Changelog | |||
|
|||
*Hans Lemuet* | |||
|
|||
* Add support for multiple templates. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* Add support for multiple templates. | |
* Add support for template parts. |
@@ -92,3 +92,36 @@ Component subclasses inherit the parent component's template if they don't defin | |||
class MyLinkComponent < LinkComponent | |||
end | |||
``` | |||
|
|||
## Multiple templates with arguments |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
## Multiple templates with arguments | |
## Template parts |
|
||
## Multiple templates with arguments | ||
|
||
ViewComponents can render multiple templates defined in the sidecar directory and send arguments to it: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ViewComponents can render multiple templates defined in the sidecar directory and send arguments to it: | |
ViewComponents can render multiple templates defined in the sidecar directory: |
├── ... | ||
``` | ||
|
||
Templates are compiled to methods in the format `render_#{template_basename}(locals = {})`, which can then be called in the component. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Templates are compiled to methods in the format `render_#{template_basename}(locals = {})`, which can then be called in the component. | |
Template parts can then be rendered by calling `render_#{template_basename}`. Template parts accept a single positional argument that is accessible in the template as `locals`, defaulting to an empty hash if no argument is passed. |
@@ -331,7 +331,7 @@ def _sidecar_files(extensions) | |||
# view files in the same directory as the component | |||
sidecar_files = Dir["#{directory}/#{component_name}.*{#{extensions}}"] | |||
|
|||
sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"] | |||
sidecar_directory_files = Dir["#{directory}/#{component_name}/*.*{#{extensions}}"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if there is a slot/subcomponent with a template in the Sidecar directory? How can we prevent/manage conflicts between the two?
Thanks for opening this PR and taking this on! Similar to how we handled Slots V2, is there a way we could mark this functionality as experimental and have components opt-in to this feature? I think there's a lot of value in trying this functionality out in real applications and making modifications as necessary before making this a stable, long-term API. |
@BlakeWilliams and I were chatting about this PR today and realized that there might be another option we could consider: private slots with templates but no sub-component, where the slot automatically accepts a single argument For example, we might have: # table_component.rb
class TableComponent < ViewComponent::Base
renders_many :rows, private: true
def initialize(items)
items.each do |item|
row({ title: item.title, author: item.author })
end
end
end <!-- table_component.html.erb -->
<% rows.each do |row| %>
<%= row %>
<% end %> <!-- table_component/row.html.erb -->
<tr>
<td><%= locals[:title] %></td>
<td><%= locals[:author] %></td>
</tr> I believe this would fulfill the desire for a private API and it would avoid allocating a new component for each row. What do y'all think? |
@joelhawksley What should the |
@fsateler yes, I think you'd have an array of HTML-safe strings. Given the concerns about performance that are driving this work, I think it might be wise for us to implement a benchmark as part of this PR that compares the number of allocations with the new approach vs. an equivalent case with rendering a component for each row. |
@joelhawksley I feel this proposal is not very railsy. In particular, the loop in the initializer feels like boilerplate. I don't really like the API. Moreover, I think this proposal would require to have something like what this PR is proposing and on top adding the private slot API |
@fsateler I think it would also be possible to use the plural slot setter if you didn't want to decompose # table_component.rb
class TableComponent < ViewComponent::Base
renders_many :rows, private: true
def initialize(items)
rows(items)
end
end The difference between ☝🏻 and the proposal in this PR is that it remains inside of the existing Slots API, which I think would help reduce potential confusion around its introduction and use. |
Has this moved along at all recently? I have a real need to be able to dynamically determine the template to render within a component, and this PR seems to enable that. Would be great to get something out, and I would be happy to help. |
Not a problem on my side! If this helps things move forward, please go ahead! |
Hi! I've been thinking about @joelhawksley 's proposal for a new API. I don't think it's going to work, not without a lot of refactoring:
This means we will create an intermediate array of intermediate objects, which is what we wanted to avoid. Moreover, we still would need a lower-level api for actually rendering the template (which would be used by the slot interface). Please lets move forward with this as the lower-level API 🙏 |
Summary
Fixes #387
This PR builds on the branch by @joelhawksley, @fermion, @fsateler and adds the ability to specify arguments for the generated functions, based on what was discussed #387 (comment). In this PR:
1- Each template would define a method called
render_<template_name>(locals = {})
2- We will not be defining the locals inside the generated method like rails does. They would need to be accessed with locals[:foo]
The logic being that one may want to use templates to extract sections, without wanting to build a full component. For example, I could have a Table component, and I would want to have the row definition in a sidecar template. This requires being able to pass in each item for every iteration: